/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package negannotation

import (
	"encoding/json"
	"errors"
	"fmt"

	v1 "k8s.io/api/core/v1"
)

// NEGAnnotationKey is the annotation key to enable GCE NEG.
// The value of the annotation must be a valid JSON string in the format
// specified by type NegAnnotation. To enable, must have either Ingress: true
// or a non-empty ExposedPorts map referencing valid ServicePorts.
// examples:
// - `{"exposed_ports":{"80":{},"443":{}}}`
// - `{"ingress":true}`
// - `{"ingress": true,"exposed_ports":{"3000":{},"4000":{}}}`
const NEGAnnotationKey = "cloud.google.com/neg"

// NEGStatusKey is the annotation key whose value is the status of the NEGs
// on the Service, and is applied by the NEG Controller.
const NEGStatusKey = "cloud.google.com/neg-status"

var (
	ErrNEGAnnotationInvalid = errors.New("NEG annotation is invalid.")
)

// Service represents Service annotations.
type Service struct {
	v map[string]string
}

// FromService extracts the annotations from an Service definition.
func FromService(obj *v1.Service) *Service {
	return &Service{obj.Annotations}
}

// PortNegMap is the mapping between service port to NEG name
type PortNegMap map[string]string

// NegAnnotation is the format of the annotation associated with the
// NEGAnnotationKey key.
type NegAnnotation struct {
	// "Ingress" indicates whether to enable NEG feature for Ingress referencing
	// the service. Each NEG correspond to a service port.
	// NEGs will be created and managed under the following conditions:
	// 1. Service is referenced by ingress
	// 2. "ingress" is set to "true". Default to "false"
	// When the above conditions are satisfied, Ingress will create a load balancer
	//  and target corresponding NEGs as backends. Service Nodeport is not required.
	Ingress bool `json:"ingress,omitempty"`
	// ExposedPorts specifies the service ports to be exposed as stand-alone NEG.
	// The exposed NEGs will be created and managed by NEG controller.
	// ExposedPorts maps ServicePort to attributes of the NEG that should be
	// associated with the ServicePort.
	ExposedPorts map[int32]NegAttributes `json:"exposed_ports,omitempty"`
}

// NegAttributes houses the attributes of the NEGs that are associated with the
// service. Future extensions to the Expose NEGs annotation should be added here.
type NegAttributes struct {
	// Note - in the future, this will be used for custom naming of NEGs.
	// Currently has no effect.
	Name string `json:"name,omitempty"`
}

// NEGEnabledForIngress returns true if the annotation is to be applied on
// Ingress-referenced ports
func (n *NegAnnotation) NEGEnabledForIngress() bool {
	return n.Ingress
}

// NEGExposed is true if the service exposes NEGs
func (n *NegAnnotation) NEGExposed() bool {
	return len(n.ExposedPorts) > 0
}

// NEGExposed is true if the service uses NEG
func (n *NegAnnotation) NEGEnabled() bool {
	return n.NEGEnabledForIngress() || n.NEGExposed()
}

func (n *NegAnnotation) String() string {
	bytes, _ := json.Marshal(n)
	return string(bytes)
}

// NegStatus contains name and zone of the Network Endpoint Group
// resources associated with this service
type NegStatus struct {
	// NetworkEndpointGroups returns the mapping between service port and NEG
	// resource. key is service port, value is the name of the NEG resource.
	NetworkEndpointGroups PortNegMap `json:"network_endpoint_groups,omitempty"`
	// Zones is a list of zones where the NEGs exist.
	Zones []string `json:"zones,omitempty"`
}

// NewNegStatus generates a NegStatus denoting the current NEGs
// associated with the given ports.
func NewNegStatus(zones []string, portToNegs PortNegMap) NegStatus {
	res := NegStatus{}
	res.Zones = zones
	res.NetworkEndpointGroups = portToNegs
	return res
}

func (ns NegStatus) Marshal() (string, error) {
	ret := ""
	bytes, err := json.Marshal(ns)
	if err != nil {
		return ret, err
	}
	return string(bytes), err
}

// ParseNegStatus parses the given annotation into NEG status struct
func ParseNegStatus(annotation string) (NegStatus, error) {
	ret := &NegStatus{}
	err := json.Unmarshal([]byte(annotation), ret)
	return *ret, err
}

// NEGAnnotation returns true if NEG annotation is found.
// If found, it also returns NEG annotation struct.
func (svc *Service) NEGAnnotation() (*NegAnnotation, bool, error) {
	var res NegAnnotation
	annotation, ok := svc.v[NEGAnnotationKey]
	if !ok {
		return nil, false, nil
	}

	// TODO: add link to Expose NEG documentation when complete
	if err := json.Unmarshal([]byte(annotation), &res); err != nil {
		return nil, true, ErrNEGAnnotationInvalid
	}

	return &res, true, nil
}

func (svc *Service) NEGStatus() (*NegStatus, bool, error) {
	var res NegStatus
	var err error

	annotation, ok := svc.v[NEGStatusKey]
	if !ok {
		return nil, false, nil
	}

	if res, err = ParseNegStatus(annotation); err != nil {
		return nil, true, fmt.Errorf("Error parsing neg status: %v", err)
	}

	return &res, true, nil
}
